/*
 * Copyright (C) 2011, EADS France
 * 
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * 
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 */
package xowl.gmi.view.diagram;

import java.util.List;
import xowl.gmi.environment.Locale;
import xowl.gmi.environment.Options;

/**
 *
 * @author Laurent WOUTERS
 */
public class Diagram extends javax.swing.JPanel {
    private static final double zoomIncrement = 0.1d;
    private static final double zoomMin = 0.1d;
    private static final double zoomMax = 5.0d;
    
    private Locale locale;
    private Widget root;
    private Couple dimension;
    private DiagramStyle style;
    private double zoom;
    // Diagram volatile state
    private UserState state;
    private List<Widget> stack;
    private Widget currentDragable;
    private Widget currentDropable;
    private Widget currentHovered;
    private Widget currentSelected;
    private Widget currentConnecting;
    // Drag and drop handling data
    private javax.swing.TransferHandler transferHandler;
    private Couple dragLocation;
    // Overlays
    private Overlay editor;
    
    public void setRoot(Widget root) { this.root = root; }
    
    public Diagram(Options options, Locale locale) {
        this.locale = locale;
        this.dimension = new Couple(100, 100);
        this.style = new DiagramStyle();
        this.zoom = 1.0d;
        this.state = UserState.Normal;
        this.dragLocation = new Couple(0, 0);
        setLayout(null);
        createTransferHandler();
        listenMouseEvents();
        setTransferHandler(transferHandler);
    }
    
    public Couple toDiagramCoord(Couple point) { return new Couple((point.x/zoom), (point.y/zoom)); }
    public Couple toSwingCoord(Couple point) { return new Couple((point.x*zoom), (point.y*zoom)); }
    
    private void createTransferHandler() {
        this.transferHandler = new javax.swing.TransferHandler(null) {
            @Override public int getSourceActions(javax.swing.JComponent c) {
                if (currentDragable != null)
                    return currentDragable.onMouseDrag().getAction();
                return javax.swing.TransferHandler.NONE;
            }
            @Override public java.awt.datatransfer.Transferable createTransferable(javax.swing.JComponent c) {
                if (currentDragable == null)
                    return null;
                final xowl.gmi.view.DragHandler dragHandler = currentDragable.onMouseDrag();
                return new java.awt.datatransfer.Transferable() {
                    @Override public java.awt.datatransfer.DataFlavor[] getTransferDataFlavors() {
                        return new java.awt.datatransfer.DataFlavor[] { dragHandler.getDataFlavor() };
                    }
                    @Override public boolean isDataFlavorSupported(java.awt.datatransfer.DataFlavor flavor) {
                        return (dragHandler.getDataFlavor() == flavor);
                    }
                    @Override public Object getTransferData(java.awt.datatransfer.DataFlavor flavor) throws java.awt.datatransfer.UnsupportedFlavorException, java.io.IOException {
                        return dragHandler.getTransferable();
                    }
                };
            }
            @Override public void exportDone(javax.swing.JComponent c, java.awt.datatransfer.Transferable t, int action) {
                if (currentDragable != null) {
                    try {
                        currentDragable.onMouseDrag().onDragDropEnd(t.getTransferData(currentDragable.onMouseDrag().getDataFlavor()), action);
                    }
                    catch (java.awt.datatransfer.UnsupportedFlavorException | java.io.IOException ex) { }
                }
            }
            @Override public java.awt.Image getDragImage() {
                if (currentDragable == null)
                    return null;
                return currentDragable.onMouseDrag().getImage();
            }
            @Override public java.awt.Point getDragImageOffset() { return dragLocation.toAWTPoint(); }
            @Override public boolean canImport(javax.swing.TransferHandler.TransferSupport support) {
                stackUpdate(new Couple(support.getDropLocation().getDropPoint()), true);
                if (currentDropable == null)
                    return false;
                if ((support.getSourceDropActions() & currentDropable.onMouseDrop().getSupportedActions()) == 0)
                    return false;
                java.awt.datatransfer.DataFlavor[] flavors = support.getDataFlavors();
                for (int i=0; i!=flavors.length; i++) {
                    if (currentDropable.onMouseDrop().acceptFlavor(flavors[i])) {
                        try {
                            Object data = support.getTransferable().getTransferData(flavors[i]);
                            if (currentDropable.onMouseDrop().acceptData(flavors[i], data))
                                return true;
                        }
                        catch (java.awt.datatransfer.UnsupportedFlavorException | java.io.IOException ex) { }
                    }
                }
                return false;
            }
            @Override public boolean importData(javax.swing.TransferHandler.TransferSupport support) {
                if (currentDropable == null)
                    return false;
                if ((support.getSourceDropActions() & currentDropable.onMouseDrop().getSupportedActions()) == 0)
                    return false;
                java.awt.datatransfer.DataFlavor[] flavors = support.getDataFlavors();
                for (int i=0; i!=flavors.length; i++) {
                    if (currentDropable.onMouseDrop().acceptFlavor(flavors[i])) {
                        try {
                            Object data = support.getTransferable().getTransferData(flavors[i]);
                            if (currentDropable.onMouseDrop().acceptData(flavors[i], data)) {
                                Couple location = new Couple(support.getDropLocation().getDropPoint());
                                if (currentDropable.onMouseDrop().drop(flavors[i], data, toDiagramCoord(location).toAWTPoint()))
                                    return true;
                            }
                        }
                        catch (java.awt.datatransfer.UnsupportedFlavorException | java.io.IOException ex) { }
                    }
                }
                return false;
            }
        };
    }
    
    private void listenMouseEvents() {
        this.addMouseListener(new java.awt.event.MouseAdapter() {
            @Override public void mouseClicked(java.awt.event.MouseEvent e) {
                if (e.getClickCount() >= 2)
                    onMouseDoubleClick(toDiagramCoord(new Couple(e.getPoint())));
                else
                    onMouseClick(toDiagramCoord(new Couple(e.getPoint())));
            }
            @Override public void mousePressed(java.awt.event.MouseEvent e) {
                if (e.isPopupTrigger())
                    onMousePopup(toDiagramCoord(new Couple(e.getPoint())));
            }
            @Override public void mouseReleased(java.awt.event.MouseEvent e) {
                if (e.isPopupTrigger())
                    onMousePopup(toDiagramCoord(new Couple(e.getPoint())));
                else if (state == UserState.ConnectionEnd)
                    onMouseEndConnection(toDiagramCoord(new Couple(e.getPoint())));
            }
            @Override public void mouseExited(java.awt.event.MouseEvent e) { onMouseExited(); }
        });
        this.addMouseMotionListener(new java.awt.event.MouseMotionAdapter() {
            @Override public void mouseDragged(java.awt.event.MouseEvent e) { onMouseDragged(toDiagramCoord(new Couple(e.getPoint())), e); }
            @Override public void mouseMoved(java.awt.event.MouseEvent e) { onMouseMoved(toDiagramCoord(new Couple(e.getPoint()))); }
        });
    }
    
    private boolean isBlocked() { return (root == null || editor != null); }
    
    private void select(Widget widget) {
        if (currentSelected != null)
            currentSelected.toggleSelected();
        if (currentSelected == widget)
            currentSelected = null;
        else
            currentSelected = widget;
        if (currentSelected != null)
            currentSelected.toggleSelected();
    }
    
    private void onMouseClick(Couple location) {
        if (isBlocked()) return;
        stackUpdate(location, false);
        if (state != UserState.Normal) return;
        Widget newSelection = null;
        for (int i=stack.size()-1; i!=-1; i--) {
            Widget w = stack.get(i);
            if (newSelection == null && w.isSelectable()) newSelection = w;
        }
        select(newSelection);
        repaint();
    }
    private void onMouseDoubleClick(Couple location) {
        if (isBlocked()) return;
        stackUpdate(location, false);
        for (int i=stack.size()-1; i!=-1; i--)
            if (stack.get(i).onMouseOpenEditor(Diagram.this))
                return;
    }
    private void onMousePopup(Couple location) {
        if (isBlocked()) return;
        stackUpdate(location, false);
        dragLocation = location;
        for (int i=stack.size()-1; i!=-1; i--) {
            Widget w = stack.get(i);
            if (w.onMouseContextMenu(Diagram.this)) {
                select(w);
                repaint();
                return;
            }
        }
    }
    private void onMouseExited() {
        stackClear();
        if (state == UserState.ConnectionEnd) {
            currentConnecting.clearConnecting();
            currentConnecting = null;
            state = UserState.ConnectionStart;
        }
    }
    private void onMouseDragged(Couple location, java.awt.event.InputEvent event) {
        if (isBlocked()) return;
        stackUpdate(location, true);
        if (state == UserState.Dragging) {
            currentDragable = null;
            for (int i=stack.size()-1; i!=-1; i--) {
                Widget w = stack.get(i);
                if (w.onMouseDrag() != null) {
                    currentDragable = w;
                    break;
                }
            }
            if (currentDragable == null) return;
            dragLocation = location.minus(currentDragable.getPosition());
            currentDragable.onMouseDrag().onDragStart(dragLocation.toAWTPoint());
            transferHandler.exportAsDrag(Diagram.this, event, currentDragable.onMouseDrag().getAction());
        } else if (state == UserState.ConnectionStart) {
            currentConnecting = null;
            for (int i=stack.size()-1; i!=-1; i--) {
                Widget w = stack.get(i);
                if (w.isConnectionStart()) {
                    currentConnecting = w;
                    break;
                }
            }
            if (currentConnecting != null) {
                currentConnecting.startConnecting();
                state = UserState.ConnectionEnd;
            }
            repaint();
        }
    }
    private void onMouseMoved(Couple location) { stackUpdate(location, false); }
    private void onMouseEndConnection(Couple location) {
        if (isBlocked()) return;
        stackUpdate(location, false);
        for (int i=stack.size()-1; i!=-1; i--) {
            Widget w = stack.get(i);
            if (w.isConnectionEnd()) {
                w.endConnecting();
                break;
            }
        }
        currentConnecting.clearConnecting();
        currentConnecting = null;
        state = UserState.Normal;
        repaint();
    }
    
    private void stackUpdate(Couple point, boolean dragFlag) {
        if (isBlocked()) return;
        if (dragFlag && state == UserState.Normal) state = UserState.Dragging;
        if (!dragFlag && state == UserState.Dragging) state = UserState.Normal;
        stack = root.getStackAt(point);
        Widget newDropable = null;
        Widget newHovered = null;
        Couple local = new Couple();
        if (!stack.isEmpty())
            local = getLocal(stack, stack.get(stack.size() - 1), point);
        for (int i=stack.size()-1; i!=-1; i--) {
            Widget w = stack.get(i);
            if (newDropable == null) newDropable = (w.onMouseDrop()!=null)?w:null;
            if (newHovered == null) newHovered = (w.onMouseEnter(state, local))?w:null;
            local = local.plus(w.getPosition());
        }
        if (newDropable != currentDropable)
            currentDropable = newDropable;
        if (currentHovered != newHovered) {
            if (currentHovered != null)
                currentHovered.onMouseLeave();
            currentHovered = newHovered;
        }
        repaint();
    }
    private void stackClear() {
        if (currentHovered != null)
            currentHovered.onMouseLeave();
        stack = null;
        currentDragable = null;
        currentDropable = null;
        currentHovered = null;
        repaint();
    }
    
    @Override public java.awt.Dimension getMinimumSize() { return toSwingCoord(dimension).toAWTDim(); }
    @Override public java.awt.Dimension getPreferredSize() { return toSwingCoord(dimension).toAWTDim(); }
    @Override public java.awt.Dimension getMaximumSize() { return toSwingCoord(dimension).toAWTDim(); }
    
    @Override public void paintComponent(java.awt.Graphics g) {
        java.awt.Graphics2D g2d = (java.awt.Graphics2D)g;
        java.awt.Dimension size = getSize();
        g2d.setRenderingHint(java.awt.RenderingHints.KEY_ANTIALIASING, java.awt.RenderingHints.VALUE_ANTIALIAS_ON);
        g2d.setRenderingHint(java.awt.RenderingHints.KEY_TEXT_ANTIALIASING, java.awt.RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
        java.awt.geom.AffineTransform previous = g2d.getTransform();
        g2d.scale(zoom, zoom);
        g2d.setBackground(style.diagramBackground);
        g2d.clearRect(0, 0, size.width, size.height);
        root.paint(g2d, 1, style, new Couple(0, 0));
        if (editor != null) {
            float[] fractions = new float[] {0f, 0.5f, 1f};
            java.awt.Color[] colors = new java.awt.Color[] {
                new java.awt.Color(0, 0, 0, 0),
                new java.awt.Color(0, 0, 0, 0),
                style.diagramEditorVeil
            };
            Couple el = editor.getLocation();
            Couple ed = editor.getSize();
            java.awt.Paint gradient = new java.awt.RadialGradientPaint(new Couple(el.x + ed.x / 2, el.y + ed.y / 2).toAWTPoint(), (int)ed.x, fractions, colors);
            g2d.setPaint(gradient);
            g2d.fillRect(0, 0, size.width, size.height);
        }
        g2d.setTransform(previous);
    }
    
    public java.awt.image.BufferedImage exportBMP() { return root.exportBMP(style); }
    public org.w3c.dom.Document exportSVG() { return root.exportSVG(style); }
    
    public void onUpdated(java.awt.Dimension bounds) {
        if (isBlocked()) return;
        dimension = root.layout(new Couple(bounds));
        root.setPosition(new Couple(0, 0));
        this.revalidate();
        this.repaint();
    }
    
    public void beginConnecting() {
        if (currentConnecting != null)
            currentConnecting.clearConnecting();
        currentConnecting = null;
        state = UserState.ConnectionStart;
    }
    public void endConnecting() {
        if (currentConnecting != null)
            currentConnecting.clearConnecting();
        currentConnecting = null;
        state = UserState.Normal;
    }
    
    public double getZoom() { return zoom; }
    public void zoomIncrease() {
        zoom += zoomIncrement;
        if (zoom > zoomMax) zoom = zoomMax;
        this.revalidate();
        this.repaint();
    }
    public void zoomDecrease() {
        zoom -= zoomIncrement;
        if (zoom < zoomMin) zoom = zoomMin;
        this.revalidate();
        this.repaint();
    }
    public void zoomReset() {
        zoom = 1.0d;
        this.revalidate();
        this.repaint();
    }
    public void zoomToFit(java.awt.Dimension dim) {
        double rwidth = dim.width / dimension.x;
        double rheight = dim.height / dimension.y;
        zoom = Math.min(rwidth, rheight);
        if (zoom < zoomMin) zoom = zoomMin;
        if (zoom > zoomMax) zoom = zoomMax;
        this.revalidate();
        this.repaint();
    }
    
    public void openEditor(DataPlaceholder placeholder, String text) {
        if (editor != null && !editor.validate())
            return;
        editor = new OverlayEditor(this, locale, placeholder, getAbsolutePosition(stack, placeholder), style, text);
        insertEditor();
    }
    private void insertEditor() {
        add(editor.getComponent());
        editor.onHooked();
        validate();
        stackClear();
    }
    public void clearEditor() {
        removeAll();
        editor = null;
        validate();
        repaint();
    }
    
    public void openMenu(javax.swing.JPopupMenu menu) {
        menu.show(this, (int)(dragLocation.x*zoom), (int)(dragLocation.y*zoom));
    }
    
    private Couple getAbsolutePosition(List<Widget> stack, Widget widget) {
        int x = 0;
        int y = 0;
        for (Widget parent : stack) {
            x += parent.getPosition().x;
            y += parent.getPosition().y;
            if (parent == widget)
                break;
        }
        return new Couple(x, y);
    }
    
    private Couple getLocal(List<Widget> stack, Widget widget, Couple point) {
        Couple pos = getAbsolutePosition(stack, widget);
        return new Couple(point.x - pos.x, point.y - pos.y);
    }
}
